Here are the modifications required to implement the "Export Text" feature.
Please apply these changes within the <script> section of your code.
Add the isExportTextMode flag near your other state variables at the beginning of the script.
JavaScript
let currentBranch = [];
let currentStepIndex = 0;
let isEditingComment = false;
let isAutoPlaying = false;
let isExportTextMode = false; // Add this line
Modify the updateToolHighlights function to disable all tool buttons when isExportTextMode is true.
JavaScript
function updateToolHighlights() {
const isModified = currentStepIndex > 0 || historyFEN.fen !== INITIAL_FEN;
const highlights = {
'tool-new': isModified && !isExportTextMode,
'tool-open': !isExportTextMode,
'tool-edit': !isEditingComment && !isExportTextMode,
'tool-save': isModified && !isExportTextMode,
'tool-undo': undoStack.length > 1 && !isExportTextMode,
'tool-redo': redoStack.length > 0 && !isExportTextMode,
'tool-rotate': isRotateEnabled && !isExportTextMode,
'tool-flip': isFlipEnabled && !isExportTextMode,
'tool-exp-txt': !isExportTextMode,
'tool-exp-svg': !isExportTextMode
};
for (const [id, shouldEnable] of Object.entries(highlights)) {
const btn = document.getElementById(id);
if (btn) {
if (shouldEnable) {
btn.classList.add('enable');
} else {
btn.classList.remove('enable');
}
}
}
}
Protect all relevant event listeners by adding if (isExportTextMode) return;.
A. SVG Click Listener:
JavaScript
svgElement.addEventListener('click', (event) => {
if (isAnimating || isEditingComment || isExportTextMode) return; // Modified line
B. Step Controls:
JavaScript
stepSlider.addEventListener('input', (e) => {
if (isEditingComment || isExportTextMode) { // Modified line
e.preventDefault();
e.target.value = currentStepIndex;
return;
}
jumpToStep(parseInt(e.target.value, 10));
});
document.querySelector('.ejceesstepminus').addEventListener('click', () => {
if (isEditingComment || isExportTextMode) return; // Modified line
jumpToStep(currentStepIndex - 1);
});
document.querySelector('.ejceesstepplus').addEventListener('click', () => {
if (isEditingComment || isExportTextMode) return; // Modified line
jumpToStep(currentStepIndex + 1);
});
const startDiv = document.getElementById('record-start');
if (startDiv) {
startDiv.addEventListener('click', () => {
if (isExportTextMode) return; // Add this line
jumpToStep(0);
});
}
C. Comment Edit Toggle:
JavaScript
document.querySelector('.ejceescomment').addEventListener('click', () => {
if (isExportTextMode) return; // Add this line
if (!isEditingComment) {
isEditingComment = true;
renderNoteUI();
}
});
D. Tool Button Listeners:
JavaScript
const toolNewBtn = document.getElementById('tool-new');
if (toolNewBtn) {
toolNewBtn.addEventListener('click', () => {
if (isExportTextMode) return; // Add this line
// ... existing logic
JavaScript
document.getElementById('tool-save').addEventListener('click', () => {
if (isExportTextMode) return; // Add this line
// ... existing logic
JavaScript
document.getElementById('tool-open').addEventListener('click', () => {
if (isExportTextMode) return; // Add this line
document.getElementById('file-input').click();
});
(Replace the existing undo/redo/rotate/flip bindings with these:)
JavaScript
document.getElementById('tool-undo').addEventListener('click', () => {
if (!isExportTextMode) undo();
});
document.getElementById('tool-redo').addEventListener('click', () => {
if (!isExportTextMode) redo();
});
document.getElementById('tool-rotate').addEventListener('click', () => {
if (isExportTextMode) return;
isRotateEnabled = !isRotateEnabled;
reapplyVisualPositions();
updateToolHighlights();
});
document.getElementById('tool-flip').addEventListener('click', () => {
if (isExportTextMode) return;
isFlipEnabled = !isFlipEnabled;
reapplyVisualPositions();
updateToolHighlights();
});
E. Keyboard Support:
JavaScript
document.addEventListener('keydown', (e) => {
if (isExportTextMode) return; // Add this line
if (e.ctrlKey && e.key === 'z') {
e.preventDefault();
undo();
}
if (e.ctrlKey && e.key === 'y') {
e.preventDefault();
redo();
}
});
Add the following functions at the end of your script logic (inside DOMContentLoaded), which will handle generating the text, replacing the UI, parsing imported strings, and recreating the game sequence.
JavaScript
// --- Text Export / Import Features ---
// Extract the linear path corresponding to the currently selected branch
function getGamePath() {
let path = [];
let currentNode = historyFEN;
let branchPtr = 0;
while (currentNode && currentNode.v && currentNode.v.length > 0) {
const choice = currentBranch[branchPtr] !== undefined ? currentBranch[branchPtr] : 0;
currentNode = currentNode.v[choice];
path.push(currentNode);
branchPtr++;
}
return path;
}
function generateExportText(isEnglish) {
let path = getGamePath();
let fen = historyFEN.fen;
let text = fen + '\n';
let isRedTurn = fen.split(' ')[1] === 'w';
let moveIdx = 0;
let moveNum = 1;
while(moveIdx < path.length) {
let line = `${moveNum}. `;
if (!isRedTurn && moveIdx === 0) {
let blkNode = path[moveIdx];
let blkMove = isEnglish ? blkNode.move : NotationConverter.toChinese(blkNode.move);
line += `... ${blkMove}`;
moveIdx++;
} else {
let redNode = path[moveIdx];
let redMove = isEnglish ? redNode.move : NotationConverter.toChinese(redNode.move);
line += `${redMove}`;
moveIdx++;
if (moveIdx < path.length) {
let blkNode = path[moveIdx];
let blkMove = isEnglish ? blkNode.move : NotationConverter.toChinese(blkNode.move);
line += ` ${blkMove}`;
moveIdx++;
}
}
text += line + '\n';
moveNum++;
}
return text.trim();
}
function renderExportTextUI() {
const recordContainer = document.querySelector('.ejceesrecord');
const commentDiv = document.querySelector('.ejceescomment');
const btnDiv = document.querySelector('.ejceestextbtn');
const cnText = generateExportText(false);
const enText = generateExportText(true);
recordContainer.innerHTML = `<textarea class="ejceescomment-edit" id="export-textarea" style="width:100%; height:100%; resize:none; border:none; outline:none; background:#2a2a2a; color:#fff; padding:8px; font-family:monospace; font-size:14px; white-space:pre; overflow:auto;">${cnText}</textarea>`;
const cnBlob = new Blob([cnText], { type: 'text/plain' });
const enBlob = new Blob([enText], { type: 'text/plain' });
const now = new Date();
const timestamp =
now.getFullYear().toString().padStart(4, '0') +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0');
commentDiv.innerHTML = `
<div style="display:flex; flex-direction:column; gap:10px; padding:10px;">
<a href="${URL.createObjectURL(cnBlob)}" download="ejcees_fen_${timestamp}.txt" style="color:#66b2ff; text-decoration:none;">fen (${cnBlob.size} bytes)</a>
<a href="${URL.createObjectURL(enBlob)}" download="ejcees_en_fen_${timestamp}.txt" style="color:#66b2ff; text-decoration:none;">en_fen (${enBlob.size} bytes)</a>
</div>
`;
btnDiv.innerHTML = `
<div class="ejceestextbtninner">
<div class="note-btn btn-confirm" id="exp-confirm" title="Confirm">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</div>
<div class="note-btn btn-cancel" id="exp-cancel" title="Cancel">
<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</div>
</div>
`;
document.getElementById('exp-cancel').addEventListener('click', () => {
isExportTextMode = false;
renderRecordUI();
renderNoteUI();
updateToolHighlights();
});
document.getElementById('exp-confirm').addEventListener('click', () => {
const text = document.getElementById('export-textarea').value;
isExportTextMode = false;
importExportedText(text);
});
}
function importExportedText(text) {
text = text.trim();
if (!text) {
renderRecordUI();
renderNoteUI();
updateToolHighlights();
return;
}
// Treat as JSON structure
if (text.startsWith('{')) {
try {
const data = JSON.parse(text);
function expand(node, parentFen) {
let dc, simfen, newNode;
if (node.m) {
dc = deriveCoordsFromMove(parentFen, node.m);
simfen = simulateMove(parentFen, dc);
newNode = {
fen: simfen,
move: node.m,
lastMove: dc,
c: node.c || "",
v: []
};
} else {
newNode = { fen: parentFen, c: node.c || "", v: [] };
}
if (node.v) {
newNode.v = node.v.map(child => expand(child, newNode.fen));
}
return newNode;
}
historyFEN = expand(data, data.fen);
initBranch();
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
return;
} catch (e) {
console.log("JSON parse failed, falling back to text format.");
}
}
// Process standard text notation format
const fenMatch = text.match(/([a-zA-Z0-9/]+ [wb])/);
if (!fenMatch) {
alert("Invalid format: FEN not found.");
renderRecordUI();
renderNoteUI();
updateToolHighlights();
return;
}
let baseFen = fenMatch[1];
let lines = text.split('\n');
let movesList = [];
let currentIsRed = baseFen.includes(' w');
for (let line of lines) {
line = line.trim();
if (/^\d+\./.test(line)) {
let movesStr = line.replace(/^\d+\.\s*/, '').trim();
let tokens = movesStr.split(/\s+/);
for (let token of tokens) {
if (token === '...') continue;
let isEnglish = /^[a-zA-Z]/.test(token) || /^[+\-=1-9][a-zA-Z0-9]/.test(token);
let moveEn = token;
if (!isEnglish) {
moveEn = NotationConverter.toEnglish(token, currentIsRed);
}
movesList.push({ move: moveEn, isRed: currentIsRed });
currentIsRed = !currentIsRed;
}
}
}
let newHistoryFEN = {
fen: baseFen,
move: null,
lastMove: null,
c: "",
v: []
};
let currentNode = newHistoryFEN;
let currentFen = baseFen;
let hasError = false;
for (let i = 0; i < movesList.length; i++) {
let m = movesList[i].move;
let isRed = movesList[i].isRed;
let dc = deriveCoordsFromMove(currentFen, m, isRed);
if (!dc) {
console.error("Failed to parse move notation:", m);
hasError = true;
break;
}
let nextFen = simulateMove(currentFen, dc);
let newNode = {
fen: nextFen,
move: m,
lastMove: dc,
c: "",
v: []
};
currentNode.v.push(newNode);
currentNode = newNode;
currentFen = nextFen;
}
if (hasError && movesList.length > 0) {
alert("Parsed partially due to an invalid move notation.");
}
historyFEN = newHistoryFEN;
initBranch();
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
}
document.getElementById('tool-exp-txt').addEventListener('click', () => {
if (isExportTextMode || isEditingComment) return;
isExportTextMode = true;
updateToolHighlights();
renderExportTextUI();
});